﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.Services.Client;
using System.Linq;
using System.Net;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace AzureTableStorage
{
    public class AzureTableStorageDataSource
    {
        AzureTableStorageServiceContext _tableStorageServiceContext;

        CloudStorageAccount GetStorageAccount()
        {
            return CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("DataConnectionString"));
        }

        /// <summary>
        /// PUT HTTP requests smaller than 1460 bytes are ineffecient with nagling turned on.
        /// See: http://blogs.msdn.com/b/windowsazurestorage/archive/2010/06/25/nagle-s-algorithm-is-not-friendly-towards-small-requests.aspx
        /// </summary>
        void HandleNagleOnEndpoint(CloudStorageAccount account)
        {
            var tableServicePoint = ServicePointManager.FindServicePoint(account.TableEndpoint);
            tableServicePoint.UseNagleAlgorithm = bool.Parse(ConfigurationManager.AppSettings["UseNaglingWithTable"]);
        }

        AzureTableStorageServiceContext GetServiceContext(CloudStorageAccount account)
        {
            return new AzureTableStorageServiceContext(account.TableEndpoint.ToString(), account.Credentials);
        }


        void InitServiceContext()
        {
            var account = GetStorageAccount();

            HandleNagleOnEndpoint(account);

            _tableStorageServiceContext = GetServiceContext(account);
            _tableStorageServiceContext.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(1));
        }

        void CreateUserTableIfNotExists(CloudStorageAccount storageAccount)
        {
            storageAccount.CreateCloudTableClient().CreateTableIfNotExist(AzureTableStorageServiceContext.UserEntityName);
        }

        void CreateRoleTableIfNotExists(CloudStorageAccount storageAccount)
        {
            storageAccount.CreateCloudTableClient().CreateTableIfNotExist(AzureTableStorageServiceContext.RoleEntityName);
        }

        void CreateSessionStateEntityIfNotExists(CloudStorageAccount storageAccount)
        {
            storageAccount.CreateCloudTableClient().CreateTableIfNotExist(AzureTableStorageServiceContext.SessionStateEntityName);
        }

        public void CreateTablesIfTheyDontExists()
        {
            var storageAccount = GetStorageAccount();
            CreateUserTableIfNotExists(storageAccount);
            CreateRoleTableIfNotExists(storageAccount);
            CreateSessionStateEntityIfNotExists(storageAccount);
        }

        public AzureTableStorageDataSource()
        {
            InitServiceContext();
        }

        #region User CRUD operations

        public IEnumerable<UserEntity> GetUsers(string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Users
                          where c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<UserEntity> GetUsers(string partitionKey, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Users
                          where c.PartitionKey == partitionKey && c.RowKey == String.Empty && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<UserEntity> FindUsers(string username, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Users
                          where c.Username.Contains(username) && c.RowKey == String.Empty && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<UserEntity> FindInactiveUsers(string username, string applicationName, DateTime userInactiveSinceDate, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Users
                          where c.Username.Contains(username) && c.RowKey == String.Empty && c.ApplicationName == applicationName && c.LastActivityDate >= userInactiveSinceDate
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        /// <summary>
        /// TODO: This method could be very slow with a lot of users, so look at making this faster somehow
        /// </summary>
        /// <param name="email">User e-mail</param>
        /// <returns></returns>
        public IEnumerable<UserEntity> GetUsersByEmail(string email, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Users
                          where c.Email == email && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<UserEntity> GetOnlineUsers(DateTime compareTime, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Users
                          where c.LastActivityDate > compareTime && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public void Delete(UserEntity itemToDelete, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.Detach(itemToDelete);
            _tableStorageServiceContext.AttachTo(AzureTableStorageServiceContext.UserEntityName, itemToDelete, "*");
            _tableStorageServiceContext.DeleteObject(itemToDelete);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        public void Insert(UserEntity newItem, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.AddObject(AzureTableStorageServiceContext.UserEntityName, newItem);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        public void Update(UserEntity itemToUpdate, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.Detach(itemToUpdate);
            _tableStorageServiceContext.AttachTo(AzureTableStorageServiceContext.UserEntityName, itemToUpdate, "*");
            _tableStorageServiceContext.UpdateObject(itemToUpdate);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        #endregion

        #region Role CRUD operations

        public IEnumerable<RoleEntity> GetAllRoles(string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Roles
                          where c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<RoleEntity> GetRolesForUser(string rowKey, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Roles
                          where c.RowKey == rowKey && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<RoleEntity> GetUsersInRole(string partitionKey, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Roles
                          where c.PartitionKey == partitionKey && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public bool IsUserInRole(string partitionKey, string rowKey, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Roles
                          where c.PartitionKey == partitionKey && c.RowKey == rowKey && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            var queryResult = query.Execute();

            return (queryResult.Count() > 0);
        }

        public IEnumerable<RoleEntity> GetUserInRole(string partitionKey, string rowKey, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Roles
                          where c.PartitionKey == partitionKey && c.RowKey == rowKey && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public bool RoleExists(string partitionKey, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Roles
                          where c.PartitionKey == partitionKey && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute().Count() > 0;
        }

        public IEnumerable<RoleEntity> FindUsersInRole(string userNameToMatch, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.Roles
                          where c.UserName.Contains(userNameToMatch) && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public void Delete(RoleEntity itemToDelete, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.Detach(itemToDelete);
            _tableStorageServiceContext.AttachTo(AzureTableStorageServiceContext.RoleEntityName, itemToDelete, "*");
            _tableStorageServiceContext.DeleteObject(itemToDelete);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        public void Insert(RoleEntity newItem, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.AddObject(AzureTableStorageServiceContext.RoleEntityName, newItem);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        public void Update(RoleEntity itemToUpdate, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.Detach(itemToUpdate);
            _tableStorageServiceContext.AttachTo(AzureTableStorageServiceContext.RoleEntityName, itemToUpdate, "*");
            _tableStorageServiceContext.UpdateObject(itemToUpdate);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }
        #endregion

        #region Session state CRUD operations

        public IEnumerable<SessionStateEntity> GetSessions(string sessionId, string applicationName, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.SessionState
                          where c.PartitionKey == sessionId && c.RowKey == String.Empty && c.ApplicationName == applicationName
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<SessionStateEntity> GetSessions(string sessionId, string applicationName, int lockId, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.SessionState
                          where c.PartitionKey == sessionId && c.RowKey == String.Empty && c.ApplicationName == applicationName
                                && c.LockId == lockId
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<SessionStateEntity> GetSessions(string sessionId, string applicationName, DateTime expires, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.SessionState
                          where c.PartitionKey == sessionId && c.RowKey == String.Empty && c.ApplicationName == applicationName
                                && c.Expires > expires
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public IEnumerable<SessionStateEntity> GetSessions(string sessionId, string applicationName, bool isLocked, DateTime expires, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.SessionState
                          where c.PartitionKey == sessionId && c.RowKey == String.Empty && c.ApplicationName == applicationName
                                && c.Locked == isLocked && c.Expires > expires
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }

        public void Delete(SessionStateEntity itemToDelete, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.Detach(itemToDelete);
            _tableStorageServiceContext.AttachTo(AzureTableStorageServiceContext.SessionStateEntityName, itemToDelete, "*");
            _tableStorageServiceContext.DeleteObject(itemToDelete);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        public void Insert(SessionStateEntity newItem, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.AddObject(AzureTableStorageServiceContext.SessionStateEntityName, newItem);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        public void Update(SessionStateEntity itemToUpdate, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            _tableStorageServiceContext.Detach(itemToUpdate);
            _tableStorageServiceContext.AttachTo(AzureTableStorageServiceContext.SessionStateEntityName, itemToUpdate, "*");
            _tableStorageServiceContext.UpdateObject(itemToUpdate);
            _tableStorageServiceContext.SaveChangesWithRetries();
        }

        /// <summary>
        /// Get all expired session-states
        /// TODO: This will be slow since it does a full table scan, consider adding a table indexed on expired to make this quicker
        /// </summary>
        /// <param name="applicationName">Application name</param>
        /// <param name="expires">Session-state expiration date</param>
        /// <param name="mergeOption">Client entity tracking merge option</param>
        /// <returns>Expired sessions</returns>
        public IEnumerable<SessionStateEntity> GetExpiredSessions(string applicationName, DateTime expires, MergeOption mergeOption = MergeOption.AppendOnly)
        {
            _tableStorageServiceContext.MergeOption = mergeOption;
            var results = from c in _tableStorageServiceContext.SessionState
                          where c.RowKey == String.Empty && c.ApplicationName == applicationName
                                && c.Expires <= expires
                          select c;

            var query = results.AsTableServiceQuery();
            return query.Execute();
        }
        #endregion

    }
}